/*
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jasig.cas.web.flow;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;

import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.Message;
import org.jasig.cas.authentication.AuthenticationException;
import org.jasig.cas.authentication.Credential;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.UsernamePasswordCredential;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.ticket.TicketCreationException;
import org.jasig.cas.ticket.TicketException;
import org.jasig.cas.ticket.TicketGrantingTicket;
import org.jasig.cas.ticket.registry.TicketRegistry;
import org.jasig.cas.web.bind.CredentialsBinder;
import org.jasig.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.util.StringUtils;
import org.springframework.web.util.CookieGenerator;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

/**
 * 通过请求进行认证
 * 
 * @author Cre.Gu
 *
 */
public class AuthenticationViaRequestAction {
	/** Error result. */
	public static final String ERROR = "error";

	/** 将serviceTicket保存到页面上 */
	public static final String VIEWSERVICETICKET = "viewServiceTicket";

	/** 将serviceTicket保存到页面上 */
	public static final String REDIRECT = "redirect";

	/** 将ticketGrantingTicket保存到cookie */
	public static final String SENDTGT = "sentTgt";

	/** 将ticketGrantingTicket保存到cookie */
	public static final String SENDTGTFRONT = "sentTgtFront";

	/**
	 * Binder that allows additional binding of form object beyond Spring
	 * defaults.
	 */
	private CredentialsBinder credentialsBinder;

	/** Core we delegate to for handling all ticket related tasks. */
	@NotNull
	private CentralAuthenticationService centralAuthenticationService;

	/** Ticket registry used to retrieve tickets by ID. */
	@NotNull
	private TicketRegistry ticketRegistry;

	@NotNull
	private CookieGenerator warnCookieGenerator;

	/** Flag indicating whether message context contains warning messages. */
	private boolean hasWarningMessages;

	/** Logger instance. **/
	protected final Logger logger = LoggerFactory.getLogger(getClass());

	public final Event submit(final RequestContext context,
			final Credential credential, final MessageContext messageContext)
			throws Exception {
		final HttpServletRequest request = WebUtils
				.getHttpServletRequest(context);
		boolean fronted = request.getParameter("apiKey") == null ? true : false;
		context.getFlashScope().put("fronted", fronted);
		
		((UsernamePasswordCredential) credential).setUsername(request
				.getParameter("username"));
		((UsernamePasswordCredential) credential).setPassword(request
				.getParameter("password"));

		final String ticketGrantingTicketId = WebUtils
				.getTicketGrantingTicketId(context);
		final Service service = WebUtils.getService(context);

		try {
			if (ticketGrantingTicketId != null) {
				return grantST(ticketGrantingTicketId, service, credential,
						context, fronted, false);
			}
		} catch (final TicketCreationException e) {
			logger.warn("Invalid attempt to access service using renew=true with different credential. "
					+ "Ending SSO session.");
			this.centralAuthenticationService
					.destroyTicketGrantingTicket(ticketGrantingTicketId);
		} catch (final Exception e) {
			return newEvent(ERROR, e);
		}

		try {
			return createTGTAndGrantST(credential, context, messageContext,
					service, fronted);
		} catch (final Exception e) {
			return newEvent(ERROR, e);
		}
	}

	protected Event createTGTAndGrantST(Credential credential,
			RequestContext context, MessageContext messageContext,
			Service service, boolean fronted) throws TicketException,
			AuthenticationException {
		final String tgtId = this.centralAuthenticationService
				.createTicketGrantingTicket(credential);

		WebUtils.putTicketGrantingTicketInFlowScope(context, tgtId);
		putWarnCookieIfRequestParameterPresent(context);

		final TicketGrantingTicket tgt = (TicketGrantingTicket) this.ticketRegistry
				.getTicket(tgtId);
		for (final Map.Entry<String, HandlerResult> entry : tgt
				.getAuthentication().getSuccesses().entrySet()) {
			for (final Message message : entry.getValue().getWarnings()) {
				addWarningToContext(messageContext, message);
			}
		}

		return grantST(tgtId, service, credential, context, fronted, true);
	}

	protected Event grantST(String ticketGrantingTicketId, Service service,
			Credential credential, RequestContext context, boolean fronted,
			boolean sendTgt) throws TicketException, AuthenticationException {
		final String serviceTicketId = this.centralAuthenticationService
				.grantServiceTicket(ticketGrantingTicketId, service, credential);

		WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
		putWarnCookieIfRequestParameterPresent(context);

		if (fronted)
			return sendTgt ? newEvent(SENDTGTFRONT) : newEvent(REDIRECT);

		return sendTgt ? newEvent(SENDTGT) : newEvent(VIEWSERVICETICKET);
	}

	private void putWarnCookieIfRequestParameterPresent(
			final RequestContext context) {
		final HttpServletResponse response = WebUtils
				.getHttpServletResponse(context);

		if (StringUtils.hasText(context.getExternalContext()
				.getRequestParameterMap().get("warn"))) {
			this.warnCookieGenerator.addCookie(response, "true");
		} else {
			this.warnCookieGenerator.removeCookie(response);
		}
	}

	private Event newEvent(final String id) {
		return new Event(this, id);
	}

	private Event newEvent(final String id, final Exception error) {
		return new Event(this, id, new LocalAttributeMap("error", error));
	}

	public final void setCentralAuthenticationService(
			final CentralAuthenticationService centralAuthenticationService) {
		this.centralAuthenticationService = centralAuthenticationService;
	}

	public void setTicketRegistry(final TicketRegistry ticketRegistry) {
		this.ticketRegistry = ticketRegistry;
	}

	/**
	 * Set a CredentialsBinder for additional binding of the HttpServletRequest
	 * to the Credential instance, beyond our default binding of the Credential
	 * as a Form Object in Spring WebMVC parlance. By the time we invoke this
	 * CredentialsBinder, we have already engaged in default binding such that
	 * for each HttpServletRequest parameter, if there was a JavaBean property
	 * of the Credential implementation of the same name, we have set that
	 * property to be the value of the corresponding request parameter. This
	 * CredentialsBinder plugin point exists to allow consideration of things
	 * other than HttpServletRequest parameters in populating the Credential (or
	 * more sophisticated consideration of the HttpServletRequest parameters).
	 *
	 * @param credentialsBinder
	 *            the credential binder to set.
	 */
	public final void setCredentialsBinder(
			final CredentialsBinder credentialsBinder) {
		this.credentialsBinder = credentialsBinder;
	}

	public final void setWarnCookieGenerator(
			final CookieGenerator warnCookieGenerator) {
		this.warnCookieGenerator = warnCookieGenerator;
	}

	/**
	 * Adds a warning message to the message context.
	 *
	 * @param context
	 *            Message context.
	 * @param warning
	 *            Warning message.
	 */
	private void addWarningToContext(final MessageContext context,
			final Message warning) {
		final MessageBuilder builder = new MessageBuilder().warning()
				.code(warning.getCode())
				.defaultText(warning.getDefaultMessage())
				.args(warning.getParams());
		context.addMessage(builder.build());
		this.hasWarningMessages = true;
	}
}
